import os
import sys
import re
import logging
import traceback
import websockets.exceptions as wexc


logger = logging.getLogger(__name__)


class Master:
    def __init__(self, host=('localhost', 9527)):
        '''
        values:
            host - 
            db_path - 
        needs package:
            system -
            ascii_art -
        system daemon:
            server - link
            impetus - heartbeat
        data:
            spaces -
            users -
        object:
            link_ob - {link-tag: {'ob': link, 'uid': uid}, ..}
            user_ob - {uid: user-ob}
            space_ob - {(x, y, z): space_ob}
        '''
        self.host = host
        self.db_path = None

        self.system = None
        self.ascii_art = None

        self.server = None
        self.impetus = None

        self.spaces = None
        self.users = None

        self.link_ob = {}
        self.user_ob = {}
        self.space_ob = {}

        self.DEBUG = False


    def init(self):
        #for short
        self.Server = self.system.server.Server
        self.Database = self.system.database.Database
        self.Style = self.system.style.Style
        self.Space = self.system.space.Space
        self.User = self.system.user.User
        self.Cmds = self.system.cmds.cmds
        self.Impetus = self.system.impetus.Impetus
        self.Combat = self.system.combat.Combat

        #start up
        self.server = self.Server(self, self.host[0], self.host[1])
        self.server()
        self.impetus = self.Impetus(self)
        self.impetus()

        #loading
        self.load_db()

        return True


    def load_db(self):
        with self.Database(self.db_path) as db:
            self.spaces = db.get_spaces()
            self.users = db.get_users()


    def save_db(self):
        with self.Database(self.db_path) as db:
            db.save_spaces(self.spaces)
            db.save_users(self.users)


    def save_space(self, location):
        if location not in self.spaces:
            logger.error(f'No space data for {location}') 
            return False

        with self.Database(self.db_path) as db:
            db.save_space(location, self.spaces[location])

        return True

        
    def save_user(self, uid):
        if uid not in self.users:
            logger.error(f'No user data for {uid}') 
            return False

        with self.Database(self.db_path) as db:
            db.save_user(uid, self.users[uid])
        return True


    def load_space(self, location):
        with self.Database(self.db_path) as db:
            data = db.get_space(location)

        return data.get(location, None)


    def load_user(self, uid):
        with self.Database(self.db_path) as db:
            data = db.get_user(uid)

        return data.get(uid, None)

        
    async def _login(self, link, tag):
        while True:
            await self.send(link, 'Username: ')
            uid = await link.recv()
            uid = uid.strip()
            if not uid: continue

            x_uid = self.Style.f_uid(uid)

            if uid not in self.users:
                is_new = await self.send_and_resp(link, f'Hi, {x_uid}. New friend? (Yes or No) ')
                if is_new.upper() not in ['Y', 'YES']: continue
                await self.send(link, f'Hey there, {x_uid}! Welcome to the gang!\n')
            else:
                await self.send(link, f'Welcome back, {x_uid}!\n')

            break

        return uid


    async def new_link(self, link, tag):
        '''
        Argv:
            link - link object, ex. websocket
            tag - link's tag, ex. path of websocket
        '''
        logger.debug(f'{tag} is connecting...')
        uid = await self._login(link, tag)
        if uid in [_['uid'] for _ in self.link_ob.values()]:
            x_uid = self.Style.f_uid(uid)
            await self.send(link, f'{x_uid} already in the world. Byebye!\n')
            return False

        self.link_ob[tag] = {'link': link, 'uid': uid}

        uob = self.User(self, uid)
        uob.tag = tag
        uob.link = link
        location = uob.data.get('location', None)
        if not location:
            location = (0, 0, 0)
            uob.data['location'] = location
        if location not in self.space_ob:
            self.space_ob[location] = self.Space(self, *location)
        self.space_ob[location].move_in(uob)
        self.user_ob[uid] = uob 

        return True


    def link_closed(self, tag, closed):
        '''
        Argv:
            closed - link's state
                True - close from client
                False - close from server, will be closed
        '''
        logger.debug(f'{tag} is closed: {closed}')
        uob = self.user_ob.pop(self.link_ob[tag]['uid'])
        location = uob.data.get('location', None)
        if location and location in self.space_ob:
            self.space_ob[location].move_out(uob)
        self.link_ob.pop(tag)
        return


    async def process(self, link, tag, msg:str):
        '''
        Return:
            'quit' - close link
        Notes:
            1. strip message
        '''
        msg = msg.strip()

        if not msg: return None

        #system alias
        if msg in self.Cmds.ALIAS: msg = self.Cmds.ALIAS[msg]

        m = msg.split(' ', 1)
        cmd = getattr(self.system.cmds, m[0], self.Cmds)
        '''
        all cmds -> def main(ob, args):
        '''
        try:
            resp = await cmd.main(self.user_ob[self.link_ob[tag]['uid']], m[1] if len(m) == 2 else None)
        except:
            logger.error(traceback.format_exc())
            resp = None
            if self.DEBUG: os._exit(1)

        return resp


    async def tell_users(self, msg, exclusion:list=[]):
        ex_tags = [_.tag for _ in exclusion]
        for tag, data in self.link_ob.items():
            if tag in ex_tags: continue
            await self.send(data['link'], msg)


    async def tell_user(self, ob, msg):
        await self.send(ob.link, msg)


    async def tell_others(self, ob, msg):
        await self.tell_users(msg, [ob])


    async def send(self, link, msg):
        '''
        process except
        '''
        try:
            await link.send(msg)
        except (wexc.ConnectionClosedError, wexc.ConnectionClosedOK):
            logger.info(f'[Except] {sys.exc_info()[1]}')
        except:
            logger.error(traceback.format_exc())
            if self.DEBUG: os._exit(1)


    async def send_and_resp(self, link, msg):
        '''
        process except
        '''
        try:
            await link.send(msg)
            resp = await link.recv()
            return resp
        except (wexc.ConnectionClosedError, wexc.ConnectionClosedOK):
            logger.info(f'[Except] {sys.exc_info()[1]}')
        except:
            logger.error(traceback.format_exc())
            if self.DEBUG: os._exit(1)


    def is_location(self, location):
        pos_re = r'\(\s*[-+]?\d+\s*,\s*[-+]?\d+\s*,\s*[-+]?\d+\s*\)'
        if type(location) != str: location = f'{location}'
        return re.fullmatch(pos_re, location)


    def get_space(self, location):
        if not self.is_location(location):
            logger.error(f'Error location: {location}\n')
            return None

        if location not in self.space_ob:
            self.space_ob[location] = self.Space(self, *location)

        return self.space_ob[location]


    def get_user(self, uid):
        return self.user_ob.get(uid, None)


